const cacheWrapperMock = {
  getSerializedObject: jest.fn(),
  setSerializedObject: jest.fn(),
  getOrAdd: jest.fn(),
};

jest.mock('@openops/server-shared', () => ({
  ...jest.requireActual('@openops/server-shared'),
  cacheWrapper: cacheWrapperMock,
}));

const openopsCommonMock = {
  ...jest.requireActual('@openops/common'),
  authenticateDefaultUserInOpenOpsTables: jest.fn(),
  getPrimaryKeyFieldFromFields: jest.fn(),
  getTableFields: jest.fn().mockResolvedValue([{}]),
  resolveTokenProvider: jest.fn(async (serverContext) => {
    return {
      getToken: () => serverContext.tablesDatabaseToken,
    };
  }),
  openopsTablesDropdownProperty: jest.fn().mockReturnValue({
    required: true,
    defaultValue: false,
    type: 'DROPDOWN',
  }),
  upsertRow: jest.fn(),
};

jest.mock('@openops/common', () => openopsCommonMock);
import { DynamicPropsValue } from '@openops/blocks-framework';
import { getFields, getTableIdByTableName } from '@openops/common';
import { nanoid } from 'nanoid';
import { updateRecordAction } from '../../src/actions/update-record-action';

describe('updateRowAction', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    openopsCommonMock.authenticateDefaultUserInOpenOpsTables.mockResolvedValue({
      tokenOrResolver: expect.objectContaining({
        getToken: expect.any(Function),
      }),
    });
  });

  test('should create action with correct properties', () => {
    expect(Object.keys(updateRecordAction.props).length).toBe(4);
    expect(updateRecordAction.props).toMatchObject({
      tableName: {
        required: true,
        type: 'DROPDOWN',
      },
      roundToFieldPrecision: {
        type: 'CHECKBOX',
        required: false,
        defaultValue: false,
      },
      rowPrimaryKey: {
        required: true,
        type: 'DYNAMIC',
      },
      fieldsProperties: {
        type: 'DYNAMIC',
        required: true,
      },
    });
  });

  test('should return rowPrimaryKey as required when field is not readonly', async () => {
    openopsCommonMock.getPrimaryKeyFieldFromFields.mockReturnValue({
      name: 'primary key field',
      read_only: false,
    });

    const context = createContext();
    const result = await updateRecordAction.props['rowPrimaryKey'].props(
      { tableName: 'Opportunity' } as DynamicPropsValue,
      context,
    );

    expect(result['rowPrimaryKey']).toMatchObject({
      displayName: 'Primary Key Value',
      required: true,
      type: 'SHORT_TEXT',
      description:
        'The primary key value of the row to update. If the row does not exist, a new row will be created.',
    });
  });

  test('should return rowPrimaryKey as not required when field is readonly', async () => {
    openopsCommonMock.getPrimaryKeyFieldFromFields.mockReturnValue({
      name: 'primary key field',
      read_only: true,
    });

    const context = createContext();
    const result = await updateRecordAction.props['rowPrimaryKey'].props(
      { tableName: 'Opportunity' } as DynamicPropsValue,
      context,
    );

    expect(result['rowPrimaryKey']).toMatchObject({
      displayName: 'Primary Key Value',
      required: false,
      type: 'SHORT_TEXT',
      description:
        'The primary key value of the row to update. If left empty, a new row will be created.',
    });
  });

  test('should authenticate', async () => {
    openopsCommonMock.getPrimaryKeyFieldFromFields.mockReturnValue({
      name: 'primary key field',
    });
    cacheWrapperMock.getOrAdd
      .mockReturnValueOnce(1)
      .mockReturnValue([{ id: 1, primary: true }]);

    openopsCommonMock.upsertRow.mockResolvedValue('mock result');

    const context = createContext();

    const result = (await updateRecordAction.run(context)) as any;

    validateWrapperCall(context);
    expect(result).toStrictEqual('mock result');
    expect(openopsCommonMock.resolveTokenProvider).toHaveBeenCalledTimes(1);
    expect(openopsCommonMock.resolveTokenProvider).toHaveBeenCalledWith({
      tablesDatabaseId: 1,
      tablesDatabaseToken: 'token',
    });
  });

  test('should invoke upsertRow operation', async () => {
    openopsCommonMock.getPrimaryKeyFieldFromFields.mockReturnValue({
      name: 'primary key field',
      type: 'text',
    });

    cacheWrapperMock.getOrAdd.mockReturnValueOnce(1).mockReturnValue([
      { id: 1, primary: true, name: 'id' },
      { id: 2, primary: false, name: 'field1' },
    ]);

    openopsCommonMock.upsertRow.mockResolvedValue('mock result');
    const context = createContext({
      tableName: 'Opportunity',
      rowPrimaryKey: { rowPrimaryKey: 'some primary key value' },
      fieldsProperties: [
        {
          fieldName: 'field1',
          newFieldValue: { newFieldValue: 'new value' },
        },
      ],
    });
    const result = (await updateRecordAction.run(context)) as any;

    validateWrapperCall(context);
    expect(openopsCommonMock.upsertRow).toHaveBeenCalledTimes(1);
    expect(openopsCommonMock.upsertRow).toHaveBeenCalledWith({
      tableId: 1,
      tokenOrResolver: expect.objectContaining({
        getToken: expect.any(Function),
      }),
      fields: {
        'primary key field': 'some primary key value',
        field1: 'new value',
      },
    });
    expect(result).toBe('mock result');
  });

  test('should fail to add field if column does not exist', async () => {
    openopsCommonMock.getPrimaryKeyFieldFromFields.mockReturnValue({
      name: 'primary key field',
    });
    cacheWrapperMock.getOrAdd
      .mockReturnValueOnce(1)
      .mockReturnValue([{ id: 1, primary: true, name: 'id' }]);

    openopsCommonMock.upsertRow.mockResolvedValue('mock result');
    const context = createContext({
      tableName: 'Opportunity',
      rowPrimaryKey: { rowPrimaryKey: 'some primary key value' },
      fieldsProperties: [
        {
          fieldName: 'field1',
          newFieldValue: { newFieldValue: 'new value' },
        },
      ],
    });

    await expect(updateRecordAction.run(context)).rejects.toThrow(
      'Column field1 does not exist in table Opportunity.',
    );

    expect(openopsCommonMock.upsertRow).not.toHaveBeenCalled();
    expect(
      openopsCommonMock.getPrimaryKeyFieldFromFields,
    ).not.toHaveBeenCalled();
    expect(
      openopsCommonMock.getPrimaryKeyFieldFromFields,
    ).not.toHaveBeenCalled();
  });

  test.each([[[]], [{}]])(
    'should throw if the primary key is not a valid string',
    async (rowPrimaryKey: any) => {
      cacheWrapperMock.getOrAdd.mockReturnValueOnce(1).mockReturnValue([
        { id: 1, primary: true, name: 'id' },
        { id: 2, primary: false, name: 'field1' },
      ]);

      const context = createContext({
        tableName: 'Opportunity',
        rowPrimaryKey: { rowPrimaryKey: rowPrimaryKey },
        fieldsProperties: [
          {
            fieldName: 'field1',
            newFieldValue: { newFieldValue: 'new value' },
          },
        ],
      });

      await expect(updateRecordAction.run(context)).rejects.toThrow(
        'The primary key should be a string',
      );

      expect(openopsCommonMock.upsertRow).not.toHaveBeenCalled();
    },
  );

  test('should not display fields that are read only or primary', async () => {
    openopsCommonMock.getTableFields.mockResolvedValue([
      { name: '1', primary: true },
      { name: '2', read_only: true },
      { name: '3', primary: false, read_only: false },
    ]);
    const context = createContext({ tableName: 'Opportunity' });

    const result = await updateRecordAction.props['fieldsProperties'].props(
      { tableName: 'Opportunity' } as DynamicPropsValue,
      context,
    );
    const prop = result['fieldsProperties'] as any;

    expect(prop['properties']['fieldName']['options']['options']).toEqual([
      {
        label: '3',
        value: '3',
      },
    ]);
  });

  test('should round numeric values when roundToFieldPrecision is true', async () => {
    openopsCommonMock.getPrimaryKeyFieldFromFields.mockReturnValue({
      name: 'id',
      type: 'number',
    });
    cacheWrapperMock.getOrAdd.mockReturnValueOnce(1).mockReturnValue([
      {
        id: 1,
        primary: true,
        name: 'id',
        type: 'number',
        number_decimal_places: 2,
      },
      {
        id: 2,
        primary: false,
        name: 'amount',
        type: 'number',
        number_decimal_places: 2,
      },
    ]);
    openopsCommonMock.upsertRow.mockResolvedValue('mock result');

    const context = createContext({
      tableName: 'Opportunity',
      rowPrimaryKey: { rowPrimaryKey: '1' },
      fieldsProperties: [
        {
          fieldName: 'amount',
          newFieldValue: { newFieldValue: 3.14159 },
        },
      ],
    });
    context.propsValue.roundToFieldPrecision = true;

    await updateRecordAction.run(context);

    expect(openopsCommonMock.upsertRow).toHaveBeenCalledWith(
      expect.objectContaining({
        fields: expect.objectContaining({
          amount: '3.14',
        }),
      }),
    );
  });

  test('should throw error for excess decimals when roundToFieldPrecision is false', async () => {
    openopsCommonMock.getPrimaryKeyFieldFromFields.mockReturnValue({
      name: 'id',
      type: 'number',
    });
    cacheWrapperMock.getOrAdd.mockReturnValueOnce(1).mockReturnValue([
      {
        id: 1,
        primary: true,
        name: 'id',
        type: 'number',
        number_decimal_places: 2,
      },
      {
        id: 2,
        primary: false,
        name: 'amount',
        type: 'number',
        number_decimal_places: 2,
      },
    ]);
    openopsCommonMock.upsertRow.mockResolvedValue('mock result');

    const context = createContext({
      tableName: 'Opportunity',
      rowPrimaryKey: { rowPrimaryKey: '1' },
      fieldsProperties: [
        {
          fieldName: 'amount',
          newFieldValue: { newFieldValue: 3.14159 },
        },
      ],
    });
    context.propsValue.roundToFieldPrecision = false;

    await expect(updateRecordAction.run(context)).rejects.toThrow(
      'Field "amount" allows 2 decimal place(s); received 3.14159. Enable "Round Numeric Values" or provide a value with at most 2 decimals.',
    );
  });
});

describe('fieldsProperties property', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('fieldsProperties should have correct properties', async () => {
    const context = createContext();

    const result = await updateRecordAction.props['fieldsProperties'].props(
      { tableName: 'Opportunity' } as DynamicPropsValue,
      context,
    );

    expect(result['fieldsProperties']).toMatchObject({
      displayName: 'Fields to update',
      type: 'ARRAY',
      valueSchema: undefined,
      properties: {
        fieldName: {
          displayName: 'Field name',
          required: true,
          valueSchema: undefined,
        },
        newFieldValue: {
          displayName: 'New field value',
          required: true,
          valueSchema: undefined,
          type: 'DYNAMIC',
        },
      },
    });
    expect(openopsCommonMock.getTableFields).toHaveBeenCalledTimes(1);
    expect(openopsCommonMock.getTableFields).toHaveBeenCalledWith(
      'Opportunity',
      {
        tablesDatabaseId: 1,
        tablesDatabaseToken: 'token',
      },
    );
  });
});

function validateWrapperCall(context: any) {
  expect(cacheWrapperMock.getOrAdd).toHaveBeenCalledTimes(2);
  expect(cacheWrapperMock.getOrAdd).toHaveBeenNthCalledWith(
    1,
    `${context.run.id}-table-${context.propsValue.tableName}`,
    getTableIdByTableName,
    [
      context.propsValue.tableName,
      {
        tablesDatabaseId: context.server.tablesDatabaseId,
        tablesDatabaseToken: context.server.tablesDatabaseToken,
      },
    ],
  );
  expect(cacheWrapperMock.getOrAdd).toHaveBeenNthCalledWith(
    2,
    `${context.run.id}-1-fields`,
    getFields,
    [
      1,
      expect.objectContaining({
        getToken: expect.any(Function),
      }),
    ],
  );
}

interface ContextParams {
  tableName?: string;
  rowPrimaryKey?: { rowPrimaryKey: string };
  fieldsProperties?: { fieldName: string; newFieldValue: any }[];
}

function createContext(params?: ContextParams) {
  return {
    ...jest.requireActual('@openops/blocks-framework'),
    propsValue: {
      tableName: params?.tableName ?? 'Opportunity',
      rowPrimaryKey: params?.rowPrimaryKey ?? {
        rowPrimaryKey: 'default primary key',
      },
      fieldsProperties: { fieldsProperties: params?.fieldsProperties },
    },
    server: {
      tablesDatabaseId: 1,
      tablesDatabaseToken: 'token',
    },
    run: {
      id: nanoid(),
    },
  };
}
